En primer lugar, se procede a cargar los datos raw obtenidos del repositorio Zenodo, creado el 7 de abril de 2022 para la Práctica 1 de esta asignatura. En dicho fichero, ya se encuentran integrados los registros obtenidos tras el proceso de scraping de Idealista y de Fotocasa, por lo que no es necesario realizar de nuevo el proceso para la fusión de los datos.
houses.raw <- read.csv('../data/real-estate-raw.csv', header=TRUE, sep=',',
stringsAsFactors=FALSE, fileEncoding='UTF-8')
houses.raw.dim <- dim(houses.raw)Se han cargado 3.544 registros con 16 campos asociados a cada uno. Así, se muestran 5 registros aleatorios para comprobar la estructura del conjunto de datos, puesto que si se seleccionan los del principio (head) o final (tail) todos serán de la misma fuente de datos:
houses.raw[sample(nrow(houses.raw), 5), ]Además, se examina el tipo de datos con los que R ha interpretado cada variable:
Form.Basic = c("striped", "hover", "condensed", "responsive")
kbl(cbind(sapply(houses.raw, class)),
caption="Tipo de datos de cada variable") %>%
kable_styling(bootstrap_options = Form.Basic) %>%
scroll_box(width = "100%",
height = "400px")| id | character |
| url | character |
| title | character |
| location | character |
| price | character |
| m2 | character |
| rooms | character |
| floor | character |
| num.photos | character |
| floor.plan | character |
| view3d | character |
| video | character |
| home.staging | character |
| description | character |
| photo_urls | character |
| source | character |
Para finalizar la fase de integración de los datos, se eliminan las
viviendas duplicadas, es decir, aquellos anuncios que
contengan la misma vivienda en venta. Para ello, se evalúa cuáles tienen
idéntico el campo description, puesto que pueden ser
anuncios diferentes (con diferente id o
source) pero con el mismo contenido. Para profundizar en el
asunto y poderlo analizar mejor se extraen en un nuevo
dataframe las viviendas duplicadas según este criterio.
duplicated.houses <- houses.raw[duplicated(houses.raw$description), ]Así, por ejemplo, consecuencia del proceso de integración se pueden encontrar anuncios procedentes de Fotocasa e Idealista que contienen la misma vivienda:
duplicated_integration <- duplicated.houses[duplicated.houses$id == '161630827' |
duplicated.houses$id == '97136644',
c('description', 'source', 'location', 'price')]
duplicated_integrationPor otro lado, se observa como fenómeno que también existen viviendas ofertadas varias veces en el mismo portal inmobiliario:
duplicated_source <- duplicated.houses[duplicated.houses$id == '157307581' |
duplicated.houses$id == '160123208'|
duplicated.houses$id == '162766808'|
duplicated.houses$id == '96250173'|
duplicated.houses$id == '91151646'|
duplicated.houses$id == '96890387'|
duplicated.houses$id == '91334123'|
duplicated.houses$id == '96857738',
c('description', 'source', 'location', 'price')]
duplicated_sourceCon esta información, se deciden eliminar las viviendas
duplicadas y, en caso de que se encuentre en ambos sitios web,
se mantiene la vivienda de Fotocasa al contener información en el campo
rooms, asunto que se trabajará más adelante. Fotocasas se
selecciona por defecto porque es el primer registro que aparece en el
dataframe original:
houses.unique = houses.raw[!duplicated(houses.raw$description),]Sin registros duplicados, se pasa a la selección de los datos de interés, que conlleva una reducción de la dimensionalidad. Se lista a continuación cada campo, una breve descripción del mismo y si se selecciona o no, así como los motivos de la decisión:
id: identificador numérico para cada
registro. Actúa como identificador único junto al campo
source. Se decide mantener los campos identificadores.url: enlace a la página de venta de la
vivienda Se descarta debido a que no se puede obtener más información de
la ya obtenida en la práctica anterior y a que los anuncios pueden haber
desaparecido.title: título del anuncio. Se descarta
por no ofrecer información adicional.location: barrio en que se encuentra
la vivienda anunciada. Puede ofrecer información relevante sobre la
variabilidad de precios en el mercado inmobiliario y permite agrupar los
anuncios por zonas. Será importante en la parte de análisis.price: precio en euros.m2: metros cuadrados de la vivienda.
Permite distinguir el tamaño de las viviendas y realizar
agrupaciones.price.m2: se creará una nueva variable
que contenga el valor del precio por metro cuadrado en euros de la
vivienda.rooms: número de habitaciones de la
vivienda. Junto a m2 permite describir sus
características físicas.floor: altura a la que se encuentra la
vivienda. Ayuda a las descripción de sus características físicas.num.photos: número de fotos que
acompañan al anuncio. Se puede analizar si existe correlación entre
algún tipo de vivienda (caras, baratas, con peores o mejores
características) y el número de fotos.floor.plan: valor lógico que indica si
se ha adjuntado el plano de la casa al anuncio.view3d: valor lógico que indica si el
anuncio cuenta con la característica de visión en 3D.video: valor lógico que indica si el
anuncio cuenta con un vídeo de la viviendahome.staging: valor lógico que indica
si el anuncio cuenta con la característica de home
staging.description: descripción del cuerpo
del anuncio. Se mantiene por si su análisis sirviera de utilidad en el
análisis futuro, aunque un análisis textual supera el contexto de la
asignatura.description_length: se creará un campo
nuevo description_length con el número de palabras
contenidas en la descripción para futuros análisis.photo_urls: lista de los enlaces de
las fotografías adjuntas al anuncio. Se descarta por no realizar un
análisis de las fotografías.source: cadena de texto que indica la
fuente de la que se ha extraído la información del anuncio. Solo existen
los valores “idealista” y “fotocasa”. Forma parte del identificador
único de cara registro y sirve, además, para agrupar viviendas.Como se ha observado que todos los campos se han codificado como cadenas de caracteres, se optimiza la fase de selección de datos realizando también la transformación del tipo de datos. Así, a continuación se eliminan las variables descartadas, se crean las nuevas y se transforman los datos y se muestran 5 registros al azar:
selected.houses <- houses.unique %>% dplyr::select(-url, -title, -photo_urls) %>%
dplyr::mutate(id = as.integer(id),
location = as.factor(location),
price = as.integer(str_remove_all(price, '\\.')),
m2 = as.integer(m2),
price.m2 = as.double(price/m2),
rooms = as.integer(rooms),
floor = as.factor(floor),
num.photos = as.integer(num.photos),
floor.plan = as.logical(as.integer(floor.plan)),
view3d = as.logical(as.integer(view3d)),
video = as.logical(as.integer(video)),
home.staging = as.logical(as.integer(home.staging)),
description = description,
description.length = nchar(description),
source = as.factor(source))
selected.houses.dim <- dim(selected.houses)
houses.raw[sample(nrow(selected.houses), 5), ]Finalmente, debido a que la cantidad de registros no es muy elevada, se decide no reducirlos mediante ningún método.
Por tanto, el conjunto de datos con el que se trabajará durante el resto del estudio está compuesto de 3.361 registros con 15 campos por cada registro. En este punto, y antes de continuar con la limpieza de datos, es interesante mostrar la estructura completa del dataframe:
str(selected.houses)## 'data.frame': 3361 obs. of 15 variables:
## $ id : int 157512027 162925661 162588489 162239807 163071926 162965614 162532531 162696838 162659376 160304048 ...
## $ location : Factor w/ 3 levels "barrio-de-salamanca",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ price : int 1090000 1400000 1490000 2150000 2550000 410000 895000 1690000 699000 1600000 ...
## $ m2 : int 130 232 147 197 274 77 173 193 108 157 ...
## $ rooms : int 2 5 1 3 4 4 3 3 3 3 ...
## $ floor : Factor w/ 69 levels "11ª","12ª","1ª",..: 9 3 9 5 4 7 4 3 7 5 ...
## $ num.photos : int 29 16 50 51 40 24 41 38 47 15 ...
## $ floor.plan : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
## $ view3d : logi FALSE FALSE TRUE FALSE FALSE FALSE ...
## $ video : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
## $ home.staging : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
## $ description : chr "Maravillo ático ubicado en un edificio clásico del año 1923 recién rehabilitado, manteniendo sus originales pat"| __truncated__ "Fortuny Real Estate vende vivienda ubicada en un prestigioso y señorial edificio de mediados de siglo.\n\nSe tr"| __truncated__ "Uptown Real Estate presenta en exclusiva este espectacular ático a estrenar en la mejor ubicación de Recoletos,"| __truncated__ "CALLE VELAZQAUEZ JUNTO AL RETIRO, 3 BALCONES A LA CALLE.\nPropiedad a estrenar, situada en un edificio clásico "| __truncated__ ...
## $ source : Factor w/ 2 levels "fotocasa","idealista": 1 1 1 1 1 1 1 1 1 1 ...
## $ price.m2 : num 8385 6034 10136 10914 9307 ...
## $ description.length: int 758 1032 2605 1630 1191 659 3186 1048 1419 2317 ...
Tras haber seleccionado los datos útiles, el siguiente paso hacia el análisis consiste en la limpieza y normalización de los datos. En esta fase, es necesario gestionar los registros con valores perdidos o missing data, así como comprobar la distribución de ciertos tipos de datos (por ejemplo, el precio de la vivienda o los \(m^2\)) para inspeccionar la existencia de valores extremos o outliers. Sin embargo, tanto en la visualización de registros aleatorios del dataframe como en el estudio de la estructura, se ha detectado que hay campos que todavía deben tratarse mediante un proceso de transformación previa ya que su resultado puede afectar tanto a la gestión de missing data como de outliers.
Varios de los campos del dataset son factores y pueden albergar valores ligeramente distintos que hagan referencia al mismo concepto. Se procede a comprobar los valores de estos campos para decidir sobre su necesidad de unificarlos:
levels(selected.houses$location)## [1] "barrio-de-salamanca" "location" "villaverde"
levels(selected.houses$floor)## [1] "11ª" "12ª"
## [3] "1ª" "2ª"
## [5] "3ª" "4ª"
## [7] "5ª" "6ª"
## [9] "7ª" "8ª"
## [11] "9ª" "floor"
## [13] "Planta -1 interior" "Planta 10ª exterior con ascensor"
## [15] "Planta 11ª exterior con ascensor" "Planta 12ª"
## [17] "Planta 12ª exterior con ascensor" "Planta 13ª exterior con ascensor"
## [19] "Planta 1ª" "Planta 1ª con ascensor"
## [21] "Planta 1ª exterior" "Planta 1ª exterior con ascensor"
## [23] "Planta 1ª exterior sin ascensor" "Planta 1ª interior con ascensor"
## [25] "Planta 1ª interior sin ascensor" "Planta 1ª sin ascensor"
## [27] "Planta 2ª" "Planta 2ª con ascensor"
## [29] "Planta 2ª exterior" "Planta 2ª exterior con ascensor"
## [31] "Planta 2ª exterior sin ascensor" "Planta 2ª interior"
## [33] "Planta 2ª interior con ascensor" "Planta 2ª interior sin ascensor"
## [35] "Planta 2ª sin ascensor" "Planta 3ª"
## [37] "Planta 3ª con ascensor" "Planta 3ª exterior"
## [39] "Planta 3ª exterior con ascensor" "Planta 3ª exterior sin ascensor"
## [41] "Planta 3ª interior con ascensor" "Planta 3ª interior sin ascensor"
## [43] "Planta 3ª sin ascensor" "Planta 4ª"
## [45] "Planta 4ª con ascensor" "Planta 4ª exterior"
## [47] "Planta 4ª exterior con ascensor" "Planta 4ª exterior sin ascensor"
## [49] "Planta 4ª interior con ascensor" "Planta 4ª interior sin ascensor"
## [51] "Planta 4ª sin ascensor" "Planta 5ª con ascensor"
## [53] "Planta 5ª exterior" "Planta 5ª exterior con ascensor"
## [55] "Planta 5ª exterior sin ascensor" "Planta 5ª interior con ascensor"
## [57] "Planta 5ª sin ascensor" "Planta 6ª exterior con ascensor"
## [59] "Planta 6ª exterior sin ascensor" "Planta 6ª interior con ascensor"
## [61] "Planta 7ª exterior con ascensor" "Planta 7ª exterior sin ascensor"
## [63] "Planta 7ª interior con ascensor" "Planta 8ª"
## [65] "Planta 8ª exterior con ascensor" "Planta 8ª interior con ascensor"
## [67] "Planta 9ª exterior con ascensor" "Planta 9ª interior con ascensor"
## [69] "Sin planta"
levels(selected.houses$source)## [1] "fotocasa" "idealista"
El campo source contiene solo los
valores esperados. El campo location
contiene un nivel incorrecto y el campo
floor contiene información redundante en
varios niveles, además de un nivel incorrecto. Se decide descartar la
información de si la vivienda es interior o exterior y si posee o no
ascensor contenida en floor, dejando como
valor de floor su planta, cuya información
será útil en futuros análisis.
Para ello, en primer lugar se eliminan los niveles
incorrectos de location y
floor:
selected.houses <- droplevels.data.frame(selected.houses)A continuación, se modifica el campo
floor para obtener únicamente la
planta de la vivienda. Si la vivienda no tiene planta
(el caso de “Sin planta”), se le asigna la planta 0, conclusión a la que
se llega tras la exploración de los datos y verificación por la
información del campo description:
selected.houses <- selected.houses %>%
dplyr::mutate(floor=as.character(floor)) %>%
dplyr::mutate(floor=ifelse(floor=='Sin planta', '0', floor)) %>%
dplyr::mutate(floor=readr::parse_number(floor)) %>%
dplyr::mutate(floor=as.factor(floor))
levels(selected.houses$floor)## [1] "-1" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13"
Se procede a realizar un análisis preliminar y superficial de los valores perdidos en todos los campos:
kable(colSums(is.na(selected.houses)),
digits=2,
align='l',
caption="Valores nulos en cada variable") %>%
kable_styling(bootstrap_options = Form.Basic) %>%
scroll_box(width = "100%", height = "470px")| x | |
|---|---|
| id | 1 |
| location | 0 |
| price | 2 |
| m2 | 1 |
| rooms | 2370 |
| floor | 1 |
| num.photos | 1 |
| floor.plan | 1 |
| view3d | 1 |
| video | 1 |
| home.staging | 1 |
| description | 0 |
| source | 0 |
| price.m2 | 2 |
| description.length | 0 |
Como se puede observar, el campo rooms es el que tiene
más valores perdidos, por lo que se tratará más adelante. Así, se
visualizan los registros que contienen valores perdidos en las variables
id, price y
floor:
selected.houses[is.na(selected.houses$id) |
is.na(selected.houses$price) |
is.na(selected.houses$floor),]Solo existe 1 registro sin identificador,** 2 registro** sin precio y 1 sin planta. Dadas las características de los registros (demasiados campos con valores perdidos y otros campos con valores incorrectos en el registro sin identificador) se decide eliminar los registros.
selected.houses <- selected.houses[!is.na(selected.houses$id) &
!is.na(selected.houses$price) &
!is.na(selected.houses$floor),]Se procede ahora a analizar los registros con valores perdidos en el
campo rooms y, para ello, se decide
mostrar todos los registros en busca de un patrón:
selected.houses[is.na(selected.houses$rooms),]Se trata de 2.369 registros que no tienen valor en
el campo rooms. Se trata del
70% del total de registros pero, se observa claramente
que se trata de viviendas que proceden de Idealista. Se
verifica dicha suposición:
if(length(which(is.na(selected.houses$rooms))) ==
length(selected.houses$source[selected.houses$source == 'idealista'])){
print("El número de viviendas sin valor en 'rooms' SÍ es igual al de procedentes de Idealista.")
} else {
print("El número de viviendas sin valor en 'rooms' NO es igual al de procedentes de Idealista.")
}## [1] "El número de viviendas sin valor en 'rooms' SÍ es igual al de procedentes de Idealista."
Por tanto, se decide mantener todos los registros
sin valor en rooms y se tiene en cuenta
para futuros análisis que si se quiere emplear esta variable solamente
se podrán analizar las viviendas procedentes del portal web de
Fotocasa. Otras opciones habrían contemplado eliminar
el campo completamente del dataset, predecir el número de
habitaciones dado el resto de características o imputarles un valor
arbitrario, como 0.
Tras el trabajo que se ha realizado con los datos, se puede deducir
que los valores extremos, o outliers, solo
pueden encontrarse en las variables price,
m2, rooms,
floor (en este momento categórica),
num.photos,price.m2
y description.length.
Como en la primera entrega de este proyecto (Práctica 1) ya se determinó que los barrios de Salamanca y Villaverde de Madrid se encuentran en los extremos de precios dentro de la ciudad según todas las fuentes consultadas, para no distorsionar el análisis de outliers, se opta por estudiar cada variable según la ubicación de la vivienda.
Para explorar cada uno de los casos, se realiza una aproximación gráfica a cada problema mediante boxplots o diagramas de caja interactivos que facilitan la identificación de los registros, comprensión e identificación, y se decide qué hacer con los candidatos a outliers.
En primer lugar, se analiza el precio:
plot_ly(y = ~ selected.houses$price,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list(type = "log",
title = "Precio en escala logarítmica"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución del precio según el barrio")Que haya viviendas caras hasta tal punto que su precio parezca un posible valor extremo equivocado no debe sorprender en este proyecto debido a que el mercado inmobiliario se caracteriza precisamente por ello. En cualquier caso, para verificar que estos precios son compatibles con la realidad, se opta por extraer los registros de las viviendas sospechosas:
villaverde.price <- selected.houses[selected.houses$price %in%
boxplot.stats(selected.houses$price[selected.houses$location ==
'villaverde'])$out &
selected.houses$location == 'villaverde' ,]
salamanca.price <- selected.houses[selected.houses$price %in%
boxplot.stats(selected.houses$price[selected.houses$location ==
'barrio-de-salamanca'])$out &
selected.houses$location == 'barrio-de-salamanca' ,]Tras estudiar los 165 registros en profundidad, se descubre que en ambos barrios los viviendas consideradas atípicamente caras también son muy grandes para la zona en la que se encuentran:
min(villaverde.price$m2)## [1] 88
min(salamanca.price$m2)## [1] 209
Por tanto se decide no tratar dichos registros al encontrarse dentro del rango de lo posible en este contexto, como se puede deducir del tamaño mínimo de estas viviendas.
A continuación se analiza el precio por metro cuadrado
plot_ly(y = ~ selected.houses$price.m2,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list(type = "log",
title = "Precio por metro cuadrado en escala logarítmica"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución del precio por metro cuadrado según el barrio")Como en el caso anterior, se extraen los registros de las viviendas que tienen un precio atípicamente alto o bajo en relación con sus metros cuadradados para cada uno de los barrios de Madrid analizados:
villaverde.price.m2 <- selected.houses[selected.houses$price.m2 %in%
boxplot.stats(selected.houses$price.m2[selected.houses$location ==
'villaverde'])$out &
selected.houses$location == 'villaverde' ,]
salamanca.price.m2 <- selected.houses[selected.houses$price.m2 %in%
boxplot.stats(selected.houses$price.m2[selected.houses$location ==
'barrio-de-salamanca'])$out &
selected.houses$location == 'barrio-de-salamanca' ,]Tras estudiar los 51 registros en profundidad, no se
encuentra un patrón que pudiera explicar esta situación y los valores
del resto de campos son coherentes con el ámbito de este proyecto. Si
embargo, sí que se detecta que las tres viviendas con un precio más bajo
por metro cuadradado en Villaverde no son tal, sino que se trata de
parcelas según se explica en el campo
description, por lo que se procede a su
eliminación:
selected.houses[(selected.houses$id == "162471798") |
(selected.houses$id == "159999857") |
(selected.houses$id == "162471885"),]selected.houses <- subset(selected.houses,
id != "162471798" &
id != "159999857" &
id !="162471885")Acabado el análisis sobre el precio, se pasa a estudiar el tamaño:
plot_ly(y = ~ selected.houses$m2,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list( title = "Metros cuadrados"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución de los metros cuadrados según el barrio")También se extraen los registros de las viviendas que tienen un tamaño atípicamente alto para cada uno de los barrios:
villaverde.m2 <- selected.houses[selected.houses$m2 %in%
boxplot.stats(selected.houses$m2[selected.houses$location ==
'villaverde'])$out &
selected.houses$location == 'villaverde' ,]
salamanca.m2 <- selected.houses[selected.houses$m2 %in%
boxplot.stats(selected.houses$m2[selected.houses$location ==
'barrio-de-salamanca'])$out &
selected.houses$location == 'barrio-de-salamanca' ,]Tras analizar los 122 registros no se encuentra ninguna anomalía que haga sospechar de un error en los datos y el tamaño por encima de lo esperado en la zona se debe simplemente a que se trata de viviendas grandes. De hecho, en las descripciones de varias de ellas se detalla que se pueden dividir en varias viviendas o el número de plantas que tiene dentro.
El siguiente paso es analizar el número de habitaciones teniendo en cuenta que este dato procede únicamente de las viviendas anunciadas en Fotocasa:
plot_ly(y = ~ selected.houses$rooms,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list( title = "Número de habitaciones"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución del número de habitaciones según el barrio")Se sigue el mismo procedimiento que antes, analizando por separado los posibles outliers pero, como era de esperar, las 4 viviendas con más habitaciones también son más grandes, por lo que se decide no tratar estos registros.
villaverde.rooms <- selected.houses[selected.houses$rooms %in%
boxplot.stats(selected.houses$rooms[selected.houses$location ==
'villaverde'])$out &
selected.houses$location == 'villaverde' ,]
salamanca.rooms <- selected.houses[selected.houses$rooms %in%
boxplot.stats(selected.houses$rooms[selected.houses$location ==
'barrio-de-salamanca'])$out &
selected.houses$location == 'barrio-de-salamanca' ,]A continuación se visualizan los candidatos a outliers según la planta en la que se encuentra la vivienda:
plot_ly(y = ~ selected.houses$floor,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list(title = "Planta"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución de la planta según el barrio")Según los datos del Catastro en el Barrio de Salamanca de Madrid existen edificios de 12 alturas y en el de Villaverde de 13, e incluso más en ambos casos, aunque no es lo más común. Por tanto, cabe la posibilidad de que las viviendas en venta se encuentren en dichas plantas, por lo que no es necesario profundizar en el análisis de estos datos o los registros completos.
En cuanto al número de fotografías, los boxplots resultantes son los siguientes:
plot_ly(y = ~ selected.houses$num.photos,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list(title = "Número de fotografías"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución del número de fotografías según el barrio")si bien es cierto que el número máximo de fotografías en Fotocasa es de 30, este límite asciende hasta 200 en Idealista, por lo que nada hace determinar que los valores atípicos sean indicativos de errores en el proceso de captura de los anuncios de las viviendas. De hecho, el anuncio con 194 imágenes corresponde al de una vivienda de casi 3 millones de euros, por lo que tiene sentido que sea el más completo de todos en este aspecto:
selected.houses[(selected.houses$num.photos = 194),]Por úlimo, se evalúa la variable que recoge la longitud de la descripción en caracteres:
plot_ly(y = ~ selected.houses$description.length,
color = ~ selected.houses$location,
type = "box") %>%
layout(yaxis = list(title = "Longitud de la descripción"),
xaxis = list(title = "'Location' (barrio de la vivienda)"),
title = "Distribución de la longitud del anuncio según el barrio")Como según la normativa de Fotocasa el máximo de caracteres es de 4.000 y la de Idealista sube este máximo hasta los 5.000 caracteres, los marcados como posibles outliers se consideran registros dentro de lo posible, por lo que se decide mantenerlos sin actuar sobre ellos.
Este apartado ha sido útil para descartar la eliminación de posibles outliers y verificar la presunta corrección de los datos, excepto tres viviendas que en realidad no lo eran. Además, ha servido para tener una primera aproximación sobre las características de las viviendas en función de su localización. Así, aquellas situadas en el Barrio de Salamanca de Madrid tienden a ser más caras en valores absolutos y relativos, más grandes, con más habitaciones y con anuncios más completos (en número de caracteres y fotografías) que las ubicadas en el barrio de Villaverde de la capital. En cualquier caso, en el apartado de Análisis se llegará a conclusiones más precisas.
Tras este proceso de data cleaning o limpieza de datos, el conjunto de datos final tiene las siguientes características:
options(knitr.kable.NA = '')
kable(summary(selected.houses),
digits=2,
align='l',
caption="Datos resumen de cada variable") %>%
kable_styling(bootstrap_options = Form.Basic) %>%
scroll_box(width = "100%", height = "480px")| id | location | price | m2 | rooms | floor | num.photos | floor.plan | view3d | video | home.staging | description | source | price.m2 | description.length | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Min. : 305727 | barrio-de-salamanca:2523 | Min. : 43000 | Min. : 16 | Min. : 1 | 1 :696 | Min. :194 | Mode :logical | Mode :logical | Mode :logical | Mode :logical | Length:3356 | fotocasa : 987 | Min. : 758 | Min. : 12 | |
| 1st Qu.: 95621531 | location : 0 | 1st Qu.: 244750 | 1st Qu.: 70 | 1st Qu.: 2 | 2 :628 | 1st Qu.:194 | FALSE:2367 | FALSE:3079 | FALSE:2827 | FALSE:3323 | Class :character | idealista:2369 | 1st Qu.: 3071 | 1st Qu.: 658 | |
| Median : 96925385 | villaverde : 833 | Median : 650000 | Median :106 | Median : 3 | 3 :560 | Median :194 | TRUE :989 | TRUE :277 | TRUE :529 | TRUE :33 | Mode :character | Median : 6158 | Median : 987 | ||
| Mean :113824515 | Mean : 1003208 | Mean :144 | Mean : 3 | 0 :451 | Mean :194 | Mean : 6021 | Mean :1177 | ||||||||
| 3rd Qu.:160123187 | 3rd Qu.: 1395000 | 3rd Qu.:185 | 3rd Qu.: 4 | 4 :362 | 3rd Qu.:194 | 3rd Qu.: 8365 | 3rd Qu.:1523 | ||||||||
| Max. :163163399 | Max. :15000000 | Max. :885 | Max. :14 | 5 :339 | Max. :194 | Max. :25000 | Max. :4632 | ||||||||
| NA’s :2369 | (Other):320 |
Por último, se guarda el conjunto de datos final en un nuevo fichero, que también estará disponible en el repositorio público:
write.csv(selected.houses,
file = '../data/real-estate.csv',
row.names = FALSE,
sep=',')BLABLABLA
Teniendo en cuenta los objetivos marcados en la introducción y el resultado de las exploraciones realizadas en apartados anteriores, a continuación se agrupan las viviendas según el barrio:
villaverde.houses <- selected.houses[selected.houses$location == "villaverde",]
salamanca.houses <- selected.houses[selected.houses$location == "barrio-de-salamanca",]Además, se crean nuevas variables para facilitar la realización de pruebas estadísticas que puedan llevar a conclusiones interesantes:
selected.houses$price.clasification <- as.factor(case_when(
selected.houses$price.m2 <= mean(selected.houses$price.m2) ~ "barato",
selected.houses$price.m2 > mean(selected.houses$price.m2) ~ "caro"
))
villaverde.houses$price.clasification <- as.factor(case_when(
villaverde.houses$price.m2 <= mean(villaverde.houses$price.m2) ~ "barato",
villaverde.houses$price.m2 > mean(villaverde.houses$price.m2) ~ "caro"
))
salamanca.houses$price.clasification <- as.factor(case_when(
salamanca.houses$price.m2 <= mean(salamanca.houses$price.m2) ~ "barato",
salamanca.houses$price.m2 > mean(salamanca.houses$price.m2) ~ "caro"
)) selected.houses$m2.clasification <- as.factor(case_when(
selected.houses$m2 <= mean(selected.houses$m2) ~ "pequeño",
selected.houses$m2 > mean(selected.houses$m2) ~ "grande"
))
villaverde.houses$m2.clasification <- as.factor(case_when(
villaverde.houses$m2 <= mean(villaverde.houses$m2) ~ "pequeño",
villaverde.houses$m2 > mean(villaverde.houses$m2) ~ "grande"
))
salamanca.houses$m2.clasification <- as.factor(case_when(
salamanca.houses$m2 <= mean(salamanca.houses$m2) ~ "pequeño",
salamanca.houses$m2 > mean(salamanca.houses$m2) ~ "grande"
)) selected.houses$height.clasification <- as.factor(case_when(
as.numeric(selected.houses$floor) <= 4 ~ "bajo",
as.numeric(selected.houses$floor) > 4 ~ "alto"
))
villaverde.houses$height.clasification <- as.factor(case_when(
as.numeric(villaverde.houses$floor) <= 4 ~ "bajo",
as.numeric(villaverde.houses$floor) > 4 ~ "alto"
))
salamanca.houses$height.clasification <- as.factor(case_when(
as.numeric(salamanca.houses$floor) <= 4 ~ "bajo",
as.numeric(salamanca.houses$floor) > 4 ~ "alto"
)) Gracias a las variables creadas, se realizan nuevas agrupaciones de manera que las viviendas se agrupan además en caras o baratas, grandes o pequeñas y altas o bajas teniendo como referencia para este criterio el barrio en el que se encuentren:
cheap.houses <- bind_rows(villaverde.houses[villaverde.houses$price.clasification == "barato",],
salamanca.houses[salamanca.houses$price.clasification == "barato",])
expensive.houses <- bind_rows(villaverde.houses[villaverde.houses$price.clasification == "caro",],
salamanca.houses[salamanca.houses$price.clasification == "caro",])small.houses <- bind_rows(villaverde.houses[villaverde.houses$m2.clasification == "pequeño",],
salamanca.houses[salamanca.houses$m2.clasification == "pequeño",])
big.houses <- bind_rows(villaverde.houses[villaverde.houses$m2.clasification == "grande",],
salamanca.houses[salamanca.houses$m2.clasification == "grande",])lower.floors.houses <- bind_rows(villaverde.houses[villaverde.houses$heigth.clasification == "bajo",],
salamanca.houses[salamanca.houses$heigth.clasification == "bajo",])
upper.floors.houses <- bind_rows(villaverde.houses[villaverde.houses$heigth.clasification == "alto",],
salamanca.houses[salamanca.houses$heigth.clasification == "alto",])Se procede a comprobar si las variables cuantitativas
price, m2 y
price.m2 cumplen los supuestos de
normalidad. A su vez, también se comprobará si el precio por \(m^2\) cumple el supuesto de
homocedasticidad para las viviendas agrupadas por barrio y las viviendas
agrupadas por barrio y altura.
priceSe procede a realizar las comprobaciones de normalidad en la variable
price, utilizando el test de Lilliefors y
la ayuda de gráficas auxiliares.
price.n <- length(selected.houses$price)
price.mean <- mean(selected.houses$price)
price.sd <- sd(selected.houses$price)
ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
ggplot(selected.houses, aes(x=price)) +
geom_density(mapping=aes(y=..density..), fill=default.color.main) +
geom_vline(xintercept=price.mean, size=1.05,
linetype='dashed', color='gray50') +
stat_function(fun=dnorm, args=c(mean=price.mean,
sd=price.sd),
color=default.color.secondary, size=1.15) +
no.axis.y + xlab('Precio') + ylab('') + title.centered +
ggtitle('Distribución de los precios de las viviendas',
subtitle='Respecto a una distribución normal'),
ggqqplot(selected.houses$price, color=default.color.main,
ggtheme = theme_gray(), xlab='Cuantiles teóricos',
ylab='Cuantiles de la muestra', title='Gráfico Q-Q',
shape=16) + title.centered + xlab('') + ylab('')
)lillie.test(selected.houses$price)##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: selected.houses$price
## D = 0.2, p-value <0.0000000000000002
Las gráficas muestran que el precio de las viviendas no sigue una
distribución normal; con el test de normalidad de Lilliefors se confirma
que la distribución de la variable price
(precio) no es normal (p-value < \(\alpha\) = 0.05). Sin embargo, la variable
precio parece seguir una distribución log-normal; es decir,
el logaritmo del precio puede seguir una distribución normal. Se procede
a comprobar dicha hipótesis:
selected.houses <- selected.houses %>% plyr::mutate(price.log = log(price))
price.log.n <- length(selected.houses$price.log)
price.log.mean <- mean(selected.houses$price.log)
price.log.sd <- sd(selected.houses$price.log)
ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
ggplot(selected.houses, aes(x=price.log)) +
geom_density(mapping=aes(y=..density..), fill=default.color.main) +
geom_vline(xintercept=price.log.mean, size=1.05,
linetype='dashed', color='gray50') +
stat_function(fun=dnorm, args=c(mean=price.log.mean,
sd=price.log.sd),
color=default.color.secondary, size=1.15) +
no.axis.y + xlab('Log(Precio)') + ylab('') + title.centered +
ggtitle(expression('Distribución de los precios de las viviendas'),
subtitle='Respecto a una distribución normal'),
ggqqplot(selected.houses$price.log, color=default.color.main,
ggtheme = theme_gray(), xlab='Cuantiles teóricos',
ylab='Cuantiles de la muestra', title = expression('Gráfico Q-Q')) +
title.centered + xlab('') + ylab('')
)lillie.test(selected.houses$price.log)##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: selected.houses$price.log
## D = 0.06, p-value <0.0000000000000002
La variable precio tampoco sigue una
distribución log-normal, según se aprecia tanto en los gráficos (colas
muy pesadas en el qqplot) como en el test de Lilliefors (p-value <
\(\alpha\) = 0.05). Se concluye por
tanto que la variable precio no sigue una
distribución normal.
m2Se procede a realizar las comprobaciones de normalidad en la variable
m2, utilizando el test de Lilliefors y la
ayuda de gráficas auxiliares.
m2.n <- length(selected.houses$m2)
m2.mean <- mean(selected.houses$m2)
m2.sd <- sd(selected.houses$m2)
ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
ggplot(selected.houses, aes(x=m2)) +
geom_density(mapping=aes(y=..density..), fill=default.color.main) +
geom_vline(xintercept=m2.mean, size=1.05,
linetype='dashed', color='gray50') +
stat_function(fun=dnorm, args=c(mean=m2.mean,
sd=m2.sd),
color=default.color.secondary, size=1.15) +
no.axis.y + xlab(expression('m'^2)) + ylab('') + title.centered +
ggtitle(expression('Distribución de las viviendas según m'^2),
subtitle='Respecto a una distribución normal'),
ggqqplot(selected.houses$m2, color=default.color.main,
ggtheme = theme_gray(), xlab='Cuantiles teóricos',
ylab='Cuantiles de la muestra', title='Gráfico Q-Q',
shape=16) + title.centered + xlab('') + ylab('')
)lillie.test(selected.houses$m2)##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: selected.houses$m2
## D = 0.2, p-value <0.0000000000000002
Las gráficas muestran que la superficie (\(m^2\)) de las viviendas no sigue una
distribución normal; con el test de normalidad de Lilliefors se confirma
que la distribución de la variable m2 no
es normal (p-value < \(\alpha\) =
0.05). Se concluye por tanto que la variable
m2 no sigue una distribución normal.
price.m2Se procede a realizar las comprobaciones de normalidad en la variable
price.m2, utilizando el test de Lilliefors
y la ayuda de gráficas auxiliares.
price.m2.n <- length(selected.houses$price.m2)
price.m2.mean <- mean(selected.houses$price.m2)
price.m2.sd <- sd(selected.houses$price.m2)
ggarrange(nrow=1, ncol=2, align='hv', heights=c(1, 0.75),
ggplot(selected.houses, aes(x=price.m2)) +
geom_density(mapping=aes(y=..density..), fill=default.color.main) +
geom_vline(xintercept=price.m2.mean, size=1.05,
linetype='dashed', color='gray50') +
stat_function(fun=dnorm, args=c(mean=price.m2.mean,
sd=price.m2.sd),
color=default.color.secondary, size=1.15) +
no.axis.y + xlab(expression('Precio/m'^2)) + ylab('') + title.centered +
ggtitle(expression('Distribución de los precios de las viviendas'),
subtitle='Respecto a una distribución normal'),
ggqqplot(selected.houses$price.m2, color=default.color.main,
ggtheme = theme_gray(), xlab='Cuantiles teóricos',
ylab='Cuantiles de la muestra', title=(expression('Gráfico Q-Q'))) +
title.centered + xlab('') + ylab('')
)lillie.test(selected.houses$price.m2)##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: selected.houses$price.m2
## D = 0.09, p-value <0.0000000000000002
Las gráficas muestran que el precio por metro cuadrado de las
viviendas no sigue una distribución normal; con el test de normalidad de
Lilliefors se confirma que la distribución de la variable
price.m2 no es normal (p-value < \(\alpha\) = 0.05). Se concluye por tanto que
la variable price.m2 no sigue una
distribución normal.
price.m2 por barriosSe procede a comprobar la homocedasticidad (también llamada homogeneidad de la varianza) del precio por metro cuadrado entre los barrios de Salamanca y Villaverde. Como los datos no siguen una distribución normal, se ha de utilizar el test de Fligner-Killeen, la alternativa no paramétrica al test de Levene.
fligner.test(price.m2 ~ location, data=selected.houses)##
## Fligner-Killeen test of homogeneity of variances
##
## data: price.m2 by location
## Fligner-Killeen:med chi-squared = 842, df = 1, p-value
## <0.0000000000000002
El test de Fligner-Killeen produce un p-value < \(\alpha\) = 0.05; por tanto, se rechaza la hipótesis nula de homocedasticidad y se concluye que el precio por metro cuadrado tiene varianzas estadísticamente diferentes para ambos barrios.
price.m2 por barrios y alturasSe procede a comprobar la homocedasticidad (también llamada homogeneidad de la varianza) del precio por metro cuadrado entre los barrios de Salamanca y Villaverde, según la altura a la que está situada la vivienda. Como los datos no siguen una distribución normal, se ha de utilizar el test de Fligner-Killeen, la alternativa no paramétrica al test de Levene.
selected.houses.location.height <- selected.houses %>%
dplyr::mutate(location.height.clasification=as.factor(
paste0(as.character(location),'-',as.character(height.clasification))))
fligner.test(price.m2 ~ location.height.clasification, data=selected.houses.location.height)##
## Fligner-Killeen test of homogeneity of variances
##
## data: price.m2 by location.height.clasification
## Fligner-Killeen:med chi-squared = 875, df = 3, p-value
## <0.0000000000000002
El test de Fligner-Killeen produce un p-value < \(\alpha\) = 0.05; por tanto, se rechaza la hipótesis nula de homocedasticidad y se concluye que el precio por metro cuadrado tiene varianzas estadísticamente diferentes para ambos barrios y criterios de altura.
BLABLABLÁ
Desde la concepción de este proyecto se trabajó sobre la premisa de que la mayor amplitud de diferencia de precios en la ciudad de Madrid se daba entre el Barrio de Salamanca y el de Villaverde. Sin embargo, todas las fuentes consultadas evalúan los precios en valores absolutos y, en ese sentido, cabe preguntarse si existe tal diferencia en valores relativos respecto al tamaño de las viviendas.
Por tanto, la pregunta de investigación es si existe una diferencia significativa en el precio por metro cuadrado entre las viviendas de ambos barrios. En consecuencia, se plantean las siguientes hipótesis nula y alternativa:
Como ya se ha estudiado anteriormente, la variable de
price.m2 no sigue una distribución normal
y se da heterocedasticidad. Sin embargo, como el tamaño
de las muestras es grande, y aplicando el teorema del límite
central (TLC), se puede asumir en ambos casos una distribución
aproximadamente normal.
En consecuencia, se puede realizar un test de hipótesis de dos muestras independientes sobre la media, con distribución normal, unilateral por la derecha y teniendo en cuenta que las varianzas poblacionales son desconocidas y diferentes. Por ello, se opta en primer lugar por la prueba t de Student:
t.test(salamanca.houses$price.m2,
villaverde.houses$price.m2,
alternative = "greater")##
## Welch Two Sample t-test
##
## data: salamanca.houses$price.m2 and villaverde.houses$price.m2
## t = 110, df = 3075, p-value <0.0000000000000002
## alternative hypothesis: true difference in means is greater than 0
## 95 percent confidence interval:
## 5437 Inf
## sample estimates:
## mean of x mean of y
## 7391 1871
Como era previsible el valor del
p-value es prácticamente 0, muy por debajo
de \(\alpha\) = 0.05, por lo que se
rechaza la hipótesis nula en favor de la hipótesis
alternativa.
No obstante, si no se hubiera aplicado el teorema del límite central y se hubiera atendido al resultado del test de Lilliefors, se habría tenido que aplicar un test no paramétrico. Para verificar la calidad del resultado anterior se realiza el test de Mann-Whitney:
wilcox.test(salamanca.houses$price.m2,
villaverde.houses$price.m2,
alternative = "greater")##
## Wilcoxon rank sum test with continuity correction
##
## data: salamanca.houses$price.m2 and villaverde.houses$price.m2
## W = 2096431, p-value <0.0000000000000002
## alternative hypothesis: true location shift is greater than 0
El resultado del p-value es exactamente
el mismo, por lo que se corrobora la decisión de descartar la
hipótesis nula y aceptar la hipótesis alternativa.
En conclusión, se descarta que el precio por metro cuadrado de las viviendas en ambos barrios sea el mismo y se determina que este valor es significativamente mayor en el Barrio de Salamanca que en el de Villaverde, con un nivel de confianza del 95% en el método.
A continuación se muestra cómo ambos miembros del equipo han participado en todas las tareas de este proyecto colaborativo:
| Contribuciones | Firma |
|---|---|
| Investigación previa | AGV, PLT |
| Redacción de las respuestas | AGV, PLT |
| Desarrollo del código | AGV, PLT |